package io.hepu.robotize.interceptor;

import io.hepu.robotize.page.PageRequest;
import io.hepu.robotize.page.Range;
import io.hepu.robotize.util.Jacksons;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

@Component
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PagingInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        Object paramObject = boundSql.getParameterObject();
        if (paramObject instanceof Map<?, ?>) {
            Map<String, Object> paramMap = Jacksons.map(paramObject);
            // Handle pagination request
            if (paramMap.containsKey("request")) {
                String originalSql = boundSql.getSql();
                PageRequest pageRequest = Jacksons.convert(paramMap.get("request"), PageRequest.class);
                metaObject.setValue("delegate.boundSql.sql", buildPagingSql(originalSql, pageRequest));
            }
            // Handle conditions query: list, total
            if (paramMap.containsKey("conditions")) {
                String originalSql = boundSql.getSql();
                Map<String, Object> conditions = Jacksons.map(paramMap.get("conditions"));
                metaObject.setValue("delegate.boundSql.sql", buildConditionalSql(originalSql, conditions));
            }
        }
        return invocation.proceed();
    }

    private String buildPagingSql(String originalSql, PageRequest pageRequest) {
        StringBuilder sql = new StringBuilder(originalSql);
        appendClause(sql, " WHERE ", buildWhereClause(pageRequest.getConditions()));
        appendClause(sql, " ORDER BY ", buildOrderByClause(pageRequest.getOrders()));
        sql.append(" LIMIT ").append(pageRequest.getSize());
        sql.append(" OFFSET ").append(pageRequest.getOffset());
        return sql.toString();
    }

    private String buildConditionalSql(String originalSql, Map<String, Object> conditions) {
        StringBuilder sql = new StringBuilder(originalSql);
        appendClause(sql, " WHERE ", buildWhereClause(conditions));
        return sql.toString();
    }

    private String buildWhereClause(Map<String, Object> conditions) {
        StringBuilder whereClause = new StringBuilder();
        conditions.forEach((field, value) -> {
            if (value instanceof String) {
                whereClause.append(field).append(String.format(" LIKE '%%%s%%'", value));
            } else if (value instanceof Range) {
                Range<?> range = Jacksons.convert(value, Range.class);
                whereClause.append(field).append(String.format(" BETWEEN '%s' AND '%s'", range.getStart(), range.getEnd()));
            } else {
                whereClause.append(field).append(String.format(" = #{%s}", value));
            }
            whereClause.append(" AND ");
        });
        return removeTrailing(whereClause, " AND ");
    }

    private String buildOrderByClause(Map<String, String> orders) {
        StringBuilder orderByClause = new StringBuilder();
        orders.forEach((field, direction) -> orderByClause.append(field).append(" ").append(direction).append(", "));
        return removeTrailing(orderByClause, ", ");
    }

    private void appendClause(StringBuilder sql, String clause, String condition) {
        if (!condition.isEmpty()) {
            sql.append(clause).append(condition);
        }
    }

    private String removeTrailing(StringBuilder clause, String suffix) {
        int suffixLength = suffix.length();
        if (clause.length() >= suffixLength && clause.substring(clause.length() - suffixLength).equals(suffix)) {
            clause.setLength(clause.length() - suffixLength); // Remove the suffix
        }
        return clause.toString();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // Can be used to pass some properties through configuration
    }
}
